﻿using System;
using System.CodeDom;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml;
using System.Xml.Linq;
using System.Xml.XPath;
using Microsoft.VisualStudio.BaseCustomTool;

namespace Microsoft.Services.Clab.UI.Mvvmc.VisualStudio.CustomTools
{
    //[CustomToolRegistration("SampleCustomTool", "SampleCustomTool dsscription", "9.0", "{FAE04EC1-301F-11D3-BF4B-00C04F79EFBC}", GeneratesDesignTimeSource = true)] //vs 2008 - c#
    //[CustomToolRegistration("SampleCustomTool", "SampleCustomTool dsscription", "9.0", "{164B10B9-B200-11D0-8C61-00A0C91E29D5}", GeneratesDesignTimeSource = true)] //vs 2008 - vb
    //[CustomToolRegistration("SampleCustomTool", "SampleCustomTool dsscription", "10.0", "{164B10B9-B200-11D0-8C61-00A0C91E29D5}", GeneratesDesignTimeSource = true)] // vs 2010 - vb
    [CustomToolRegistration("ClabMvvmc", "Clab Mvvmc code generator tool", "10.0", "{FAE04EC1-301F-11D3-BF4B-00C04F79EFBC}", GeneratesDesignTimeSource = true)] // vs 2010 - c#
    [Guid("A377D066-EBAE-4B20-BF5C-8F785A78ABB4")]
    [ComVisible(true)]
    public class MvvmcCodeGenerator : BaseCustomToolWithSite
    {
        private XmlNamespaceManager namespaceManager;

        protected override byte[] GenerateCode(string inputFileName, string inputFileContent)
        {
            namespaceManager = new XmlNamespaceManager(new NameTable());
            namespaceManager.AddNamespace("d", "http://Microsoft.Services.Clab/ui/2011/01/designers");

            var xDoc = XDocument.Load(new StringReader(inputFileContent));
            var xController = xDoc.XPathSelectElement("/d:mvvmc/d:codeController", namespaceManager);
            var xViewModel = xDoc.XPathSelectElement("/d:mvvmc/d:wpfViewModel", namespaceManager);
            var outputCcu = new CodeCompileUnit();

            CodeControllerGenerator.NamespaceManager = namespaceManager;
            CodeControllerGenerator.GenerateCodeController(outputCcu, xController);
            WpfViewModelGenerator.NamespaceManager = namespaceManager;
            WpfViewModelGenerator.GenerateWpfViewModel(outputCcu, xViewModel);

            #region Generate code and fix generation errors
            var sb = new StringBuilder();
            using (var sw = new StringWriter(sb))
            {
                CodeDomProvider.GenerateCodeFromCompileUnit(outputCcu, sw, null);
            }

            string outputClass = sb.ToString();

            outputClass = CodeControllerGenerator.FixCodeControllerGeneratedCode(xController, outputClass);

            return Encoding.UTF8.GetBytes(outputClass);
            #endregion
        }

        public override string GetDefaultExtension()
        {
            return ".designer.cs";
        }

        private static class CodeControllerGenerator
        {
            internal static XmlNamespaceManager NamespaceManager { get; set; }

            internal static void GenerateCodeController(CodeCompileUnit outputCcu, XElement xController)
            {
                var codeNs = new CodeNamespace(xController.GetAttributeValue("classNamespaceName"));
                var codeController = new CodeTypeDeclaration(xController.GetAttributeValue("classTypeName"));
                codeController.IsClass = true;
                codeController.IsPartial = true;
                codeController.BaseTypes.Add(new CodeTypeReference(xController.GetAttributeValue("baseControllerTypeName")));

                var codeCacheMetadata = new CodeMemberMethod() { Name = "CacheMetadata" };
                codeCacheMetadata.Attributes = MemberAttributes.Override | MemberAttributes.Family;
                codeCacheMetadata.Parameters.Add(new CodeParameterDeclarationExpression("Microsoft.Services.Clab.UI.Mvvmc.ControllerMetadata", "metadata"));

                codeController.Members.Add(codeCacheMetadata);

                foreach (var xAction in xController.XPathSelectElements("d:actions/*", NamespaceManager))
                {
                    CreateActionMethods(codeController, xAction);
                    AddCacheMetadataStatement(codeCacheMetadata, xAction);
                }

                codeNs.Types.Add(codeController);
                outputCcu.Namespaces.Add(codeNs);
            }

            /// <summary>
            /// strip {} from partial methods
            /// </summary>
            internal static string FixCodeControllerGeneratedCode(XElement xController, string outputClass)
            {
                foreach (var xAction in xController.XPathSelectElements("d:actions/*", NamespaceManager))
                {
                    //(?<good>void TestAction.*?\(.*\))(?<bad> {.*?})
                    //(?<good>void TestAction.*?\(.*?\))(?<bad>\s*{.*?})
                    var regex = new Regex(@"(?<good>void " + GetActionNameForMethodName(xAction) + @".*?\(.*?\))(?<bad>\s*{.*?})", RegexOptions.Singleline);
                    outputClass = regex.Replace(outputClass, m => m.Groups["good"].Value + ";");
                }
                return outputClass;
            }

            private static void CreateActionMethods(CodeTypeDeclaration codeController, XElement xAction)
            {
                var parameterTypes = GetParameterTypes(xAction);

                var codeExecutedMethod = new CodeMemberMethod();
                codeExecutedMethod.Name = GetActionNameForMethodName(xAction) + "Executed";
                codeExecutedMethod.ReturnType = new CodeTypeReference("partial void");
                codeExecutedMethod.Attributes = MemberAttributes.ScopeMask;

                foreach (var parameterType in parameterTypes)
                {
                    codeExecutedMethod.Parameters.Add(new CodeParameterDeclarationExpression(parameterType.Value, parameterType.Key));
                }
                codeController.Members.Add(codeExecutedMethod);

                if (xAction.GetAttributeValue("hasCanDoAction", false))
                {
                    var codeCanExecuteMethod = new CodeMemberMethod();
                    codeCanExecuteMethod.Name = GetActionNameForMethodName(xAction) + "CanExecute";
                    codeCanExecuteMethod.ReturnType = new CodeTypeReference("partial void");
                    codeCanExecuteMethod.Attributes = MemberAttributes.ScopeMask;
                    codeCanExecuteMethod.Parameters.Add(new CodeParameterDeclarationExpression(typeof(bool), "result") { Direction = FieldDirection.Ref });
                    foreach (var parameterType in parameterTypes)
                    {
                        codeCanExecuteMethod.Parameters.Add(new CodeParameterDeclarationExpression(parameterType.Value, parameterType.Key));
                    }
                    codeController.Members.Add(codeCanExecuteMethod);
                }
            }

            private static Dictionary<string, string> GetParameterTypes(XElement xAction)
            {
                var args = new Dictionary<string, string>();
                switch (xAction.Name.LocalName)
                {
                    case "action":
                        int pos = 0;
                        foreach (var xParameter in xAction.XPathSelectElements("d:parameter", NamespaceManager))
                        {
                            args.Add(xParameter.GetAttributeValue("name", string.Format("arg" + pos)), xParameter.GetAttributeValue("type"));
                            pos++;
                        }
                        break;
                    case "startupAction":
                        // do nothing, must return an empty list
                        break;
                    case "unloadViewAction":
                        args.Add("view", "Microsoft.Services.Clab.UI.Mvvmc.IView");
                        break;
                }

                return args;
            }

            private static void AddCacheMetadataStatement(CodeMemberMethod codeCacheMetadata, XElement xAction)
            {
                var parameterTypes = GetParameterTypes(xAction);

                var argRef = new CodeArgumentReferenceExpression("metadata");
                var methodRef = new CodeMethodReferenceExpression(argRef, "MapActionToMethod");
                foreach (var parameterType in parameterTypes)
                {
                    methodRef.TypeArguments.Add(parameterType.Value);
                }

                var methodInvoke = new CodeMethodInvokeExpression();
                methodInvoke.Method = methodRef;
                methodInvoke.Parameters.Add(GetActionNameCodeExpression(xAction));
                methodInvoke.Parameters.Add(new CodeMethodReferenceExpression(new CodeThisReferenceExpression(), GetActionNameForMethodName(xAction) + "Executed"));
                if (xAction.GetAttributeValue("hasCanDoAction", false))
                {
                    methodInvoke.Parameters.Add(new CodeMethodReferenceExpression(new CodeThisReferenceExpression(), GetActionNameForMethodName(xAction) + "CanExecute"));
                }

                codeCacheMetadata.Statements.Add(methodInvoke);
            }

            private static CodeExpression GetActionNameCodeExpression(XElement xAction)
            {
                switch (xAction.Name.LocalName)
                {
                    case "action":
                        {
                            return new CodePrimitiveExpression(xAction.GetAttributeValue("name"));
                        }
                    case "startupAction":
                        {
                            var typeRef = new CodeTypeReferenceExpression("Microsoft.Services.Clab.UI.Mvvmc.ControllerMetadata");
                            return new CodePropertyReferenceExpression(typeRef, "StartupActionName");
                        }
                    case "unloadViewAction":
                        {
                            var typeRef = new CodeTypeReferenceExpression("Microsoft.Services.Clab.UI.Mvvmc.ControllerMetadata");
                            return new CodePropertyReferenceExpression(typeRef, "UnloadViewActionName");
                        }
                }

                throw new ArgumentException("Invalid action type", "xAction");
            }

            private static string GetActionNameForMethodName(XElement xAction)
            {
                switch (xAction.Name.LocalName)
                {
                    case "action":
                        return xAction.GetAttributeValue("name");
                    case "startupAction":
                        return "StartupAction";
                    case "unloadViewAction":
                        return "UnloadView";
                }

                throw new ArgumentException("Invalid action type", "xAction");
            }
        }

        private static class WpfViewModelGenerator
        {
            internal static XmlNamespaceManager NamespaceManager { get; set; }

            internal static void GenerateWpfViewModel(CodeCompileUnit outputCcu, XElement xViewModel)
            {
                var createdNs = false;
                var codeNs = outputCcu.Namespaces.Cast<CodeNamespace>().FirstOrDefault(ns => ns.Name == xViewModel.GetAttributeValue("classNamespaceName"));
                if (codeNs == null)
                {
                    codeNs = new CodeNamespace(xViewModel.GetAttributeValue("classNamespaceName"));
                    createdNs = true;
                }

                var codeViewModel = new CodeTypeDeclaration(xViewModel.GetAttributeValue("classTypeName"));
                codeViewModel.IsClass = true;
                codeViewModel.IsPartial = true;
                codeViewModel.BaseTypes.Add(new CodeTypeReference(xViewModel.GetAttributeValue("baseViewModelTypeName")));

                var codeCacheMetadata = new CodeMemberMethod() { Name = "CacheMetadata" };
                codeCacheMetadata.Attributes = MemberAttributes.Override | MemberAttributes.Family;
                codeCacheMetadata.Parameters.Add(new CodeParameterDeclarationExpression("Microsoft.Services.Clab.UI.Mvvmc.Presentation.WpfViewModelMetadata", "metadata"));

                codeViewModel.Members.Add(codeCacheMetadata);

                foreach (var xProperty in xViewModel.XPathSelectElements("d:properties/d:property", NamespaceManager))
                {
                    CreateProperty(codeViewModel, xProperty);
                    AddCacheMetadatStatement(codeCacheMetadata, xProperty);
                }

                foreach (var xProperty in xViewModel.XPathSelectElements("d:initParameters/d:initParameter", NamespaceManager))
                {
                    CreateInitParameter(codeViewModel, xProperty);
                    AddCacheMetadatStatement(codeCacheMetadata, xProperty);
                }

                codeNs.Types.Add(codeViewModel);
                if (createdNs)
                {
                    outputCcu.Namespaces.Add(codeNs);
                }
            }

            private static void CreateProperty(CodeTypeDeclaration codeViewModel, XElement xProperty)
            {
                var codeProperty = new CodeMemberProperty()
                {
                    Name = xProperty.GetAttributeValue("name"),
                    Attributes = MemberAttributes.Public | MemberAttributes.Final,
                    Type = new CodeTypeReference(xProperty.GetAttributeValue("type")),
                    GetStatements =
                    {
                        new CodeMethodReturnStatement(
                            new CodeMethodInvokeExpression(
                                new CodeMethodReferenceExpression(new CodeBaseReferenceExpression(), "GetValue", new CodeTypeReference(xProperty.GetAttributeValue("type"))),
                                new CodePrimitiveExpression(xProperty.GetAttributeValue("name"))
                            ))
                    },
                    SetStatements =
                    {
                        new CodeExpressionStatement(
                            new CodeMethodInvokeExpression(
                                new CodeMethodReferenceExpression(new CodeBaseReferenceExpression(), "SetValue", new CodeTypeReference(xProperty.GetAttributeValue("type"))),
                                new CodePrimitiveExpression(xProperty.GetAttributeValue("name")),
                                new CodePropertySetValueReferenceExpression()
                            ))
                    }
                };

                codeViewModel.Members.Add(codeProperty);
            }

            private static void CreateInitParameter(CodeTypeDeclaration codeViewModel, XElement xProperty)
            {
                var codeProperty = new CodeMemberProperty()
                {
                    Name = xProperty.GetAttributeValue("name"),
                    Attributes = MemberAttributes.Public | MemberAttributes.Final,
                    Type = new CodeTypeReference(xProperty.GetAttributeValue("type")),
                    GetStatements =
                    {
                        new CodeMethodReturnStatement(
                            new CodeMethodInvokeExpression(
                                new CodeMethodReferenceExpression(new CodeBaseReferenceExpression(), "GetInitParameter", new CodeTypeReference(xProperty.GetAttributeValue("type"))),
                                new CodePrimitiveExpression(xProperty.GetAttributeValue("parameterName", xProperty.GetAttributeValue("name")))
                            ))
                    }
                };

                codeViewModel.Members.Add(codeProperty);
            }

            private static void AddCacheMetadatStatement(CodeMemberMethod codeCacheMetadata, XElement xProperty)
            {
                var argRef = new CodeArgumentReferenceExpression("metadata");
                var methodRef = new CodeMethodReferenceExpression(argRef, "DefineProperty");

                var codeNewViewModelPropety = new CodeObjectCreateExpression("Microsoft.Services.Clab.UI.Mvvmc.ViewModelProperty");
                codeNewViewModelPropety.Parameters.Add(new CodePrimitiveExpression(xProperty.GetAttributeValue("name")));
                if (Convert.ToBoolean(xProperty.GetAttributeValue("notifyPropertyChanged", true)))
                {
                    codeNewViewModelPropety.Parameters.Add(new CodePrimitiveExpression(true));
                    codeNewViewModelPropety.Parameters.Add(new CodePrimitiveExpression(null));
                }
                else
                {
                    codeNewViewModelPropety.Parameters.Add(new CodePrimitiveExpression(false));
                    codeNewViewModelPropety.Parameters.Add(new CodePrimitiveExpression(null));
                }


                var methodInvoke = new CodeMethodInvokeExpression();
                methodInvoke.Method = methodRef;
                methodInvoke.Parameters.Add(codeNewViewModelPropety);

                codeCacheMetadata.Statements.Add(methodInvoke);
            }
        }
    }
}
